﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Collections.Generic;

namespace KlearTweet.Client.Controls
{
    public class AnimatingPanelBase : Panel, IDisposable // Note: From http://labs.nerdplusart.com/
    {
        public readonly IList<UIElement> AllChildren = new List<UIElement>();

        #region Static Fields

        static int globalAnimatingCount;
        DispatcherTimer timer;

        #endregion Static Fields

        #region Fields


        bool isTimerStarted;
		protected int animatingCount { get; private set; }

        private readonly Dictionary<UIElement, ElementState> states;

        #endregion Fields

        #region Constructors

        public AnimatingPanelBase()
        {
            // make sure that the elements collection has been initialized
            states = new Dictionary<UIElement, ElementState>();
            Force = 1;
            Resistance = 0.75;// 0.25;
            Milliseconds = 2;// 10;
            SnapPrecision = 2;
            Randomness = 0.5;
            IsAnimationEnabled = true;
        }

        #endregion Constructors

        #region Properties

        static public bool GlobalIsAnimating
        {
            get { return (globalAnimatingCount != 0); }
        }

        #endregion Properties

        #region Static Methods

        private static Point GetPointFromSize(Size size)
        {
            return new Point(size.Width, size.Height);
        }

        private static Size GetSizeFromPoint(Point point)
        {
            return new Size(Math.Max(point.X, 0), Math.Max(point.Y, 0));
        }

        private static Point ToPoint(Rect r)
        {
            return new Point(r.X, r.Y);
        }

        private static Size ToSize(Rect r)
        {
            return new Size(r.Width, r.Height);
        }

        #endregion Static Methods

        #region Private Methods

        private bool RecalculateCurrentPoint(Point current, Point target, Vector velocity, double seconds, double dampening, double attractionFactor,
            out Point newCurrentPoint, out Vector newVelocity)
        {
            if (current != target)
            {
                var diff = new Vector(target.X - current.X, target.Y - current.Y);

                if (diff.Length > SnapPrecision || velocity.Length > SnapPrecision)
                {
                    velocity.X *= (1 - dampening);
                    velocity.Y *= (1 - dampening);

                    velocity += diff;

                    Vector delta = velocity * seconds * attractionFactor;

                    const double maxVelocity = 600;
                    delta *= (delta.Length > maxVelocity) ? (maxVelocity / delta.Length) : 1;

                    current.X += delta.X;
                    current.Y += delta.Y;

                    newCurrentPoint = current;
                    newVelocity = velocity;
                    return true;
                }
                //else
                {
                    newCurrentPoint = target;
                    newVelocity = new Vector(0, 0);
                    return true;
                }
            }
            //else
            {
                newCurrentPoint = current;
                newVelocity = velocity;
                return false;
            }
        }

        private void StartTimer()
        {
            if (timer == null)
            {
                timer = new DispatcherTimer();
                timer.Tick += timer_Tick;
                timer.Interval = TimeSpan.FromSeconds(1.0 / 25);
            }

            if (!isTimerStarted)
            {
                timer.Start();
                isTimerStarted = true;
            }
        }

        private void StopTimer()
        {
            if (timer != null)
            {
                timer.Stop();
                isTimerStarted = false;
            }
        }

        void timer_Tick(object sender, EventArgs e)
        {
            if (isTimerStarted)
                UpdateElementLocations();
        }

        private bool UpdateElementLocation(UIElement child, ElementState state)
        {
            if (state == null)
                return false;

            if (state.NeedsArrange)
            {
                // get the values we will need to update the element
                double force = Force;
                double resistance = Resistance;
                double seconds = 1.0 / Milliseconds;

                //look at location
                Point newCurrentLocation;
                Vector newLocationVelocity;
                bool locationChanged = RecalculateCurrentPoint(ToPoint(state.CurrentRect), ToPoint(state.TargetRect), state.LocationVelocity,
                    seconds, resistance, force * state.AttractionMultiplier,
                    out newCurrentLocation, out newLocationVelocity);

                //look at size
                Point newCurrentSize;
                Vector newSizeVelocity;
                bool sizeChanged = RecalculateCurrentPoint(GetPointFromSize(ToSize(state.CurrentRect)), GetPointFromSize(ToSize(state.TargetRect)),
                    state.SizeVelocity,
                    seconds, resistance, force * state.AttractionMultiplier,
                    out newCurrentSize, out newSizeVelocity);

                if (locationChanged || sizeChanged)
                {
                    state.CurrentRect.X = newCurrentLocation.X;
                    state.CurrentRect.Y = newCurrentLocation.Y;
                    state.LocationVelocity = newLocationVelocity;

                    Size newSizeFromPoint = GetSizeFromPoint(newCurrentSize);

                    double newWidth = Math.Max(newSizeFromPoint.Width, child.DesiredSize.Width);
                    double newHeight = Math.Max(newSizeFromPoint.Height, child.DesiredSize.Height);

                    state.CurrentRect.Width = newWidth;
                    state.CurrentRect.Height = newHeight;

                    state.SizeVelocity = newSizeVelocity;

                    return true;
                }
                //else
                {
                    state.NeedsArrange = false;
                    animatingCount--;
                    globalAnimatingCount--;

                    if (animatingCount == 0)
                    {
                        RaiseAnimationCompleted();
                    }

                    if (globalAnimatingCount == 0)
                    {
                        RaiseGlobalAnimationCompleted();
                    }

                    return false;
                }
            }

            return false;
        }

        private void UpdateElementLocations()
        {
            ElementState state;

            foreach (UIElement element in AllChildren)
            {
                if (states.ContainsKey(element))
                {
                    state = states[element];
                    if (UpdateElementLocation(element, state))
                    {
                        element.Arrange(state.CurrentRect);
                    }
                }
            }
        }

        #endregion Private Methods

        #region Public Methods

        public void RemoveState(UIElement child) // Removed so that when it is added back, the animation starts over
        {
            states.Remove(child);
        }

        public Rect GetElementCurrentRect(UIElement child)
        {
            if (states.ContainsKey(child))
            {
                return states[child].CurrentRect;
            }
            //else
            {
                return Rect.Empty;
                //throw new ArgumentException("State not available", "child");
            }
        }

        public Rect GetElementTargetRect(UIElement child)
        {
            if (states.ContainsKey(child))
            {
                return states[child].TargetRect;
            }
            //else
            {
                return Rect.Empty;
                //throw new ArgumentException("State not available", "child");
            }
        }

        public void Nudge(UIElement e)
        {
            if (!IsAnimationEnabled)
                return;
            ElementState state;
            if (!states.TryGetValue(e, out state))
                return;

            if (!state.NeedsArrange)
            {
                state.NeedsArrange = true;
                animatingCount++;
                globalAnimatingCount++;
            }
            // Choose random numbers between [-15,-5]u[5,15]
            var delta = ElementState._Random.Next(-10, 10);
            if (delta < 0) delta -= 5;
            else delta += 5;
            state.CurrentRect.X += delta;
            delta = ElementState._Random.Next(-10, 10);
            if (delta < 0) delta -= 5;
            else delta += 5;
            state.CurrentRect.Y += delta;
            // make sure that the timer is running
            StartTimer();
        }

        #endregion Public Methods

        #region Protected Methods

        protected void SetElementLocation(UIElement e, Rect finalRect)
        {
            SetElementLocation(e, finalRect, true);
        }

        protected void SetElementLocation(UIElement e, Rect finalRect, bool isAnimated)
        {
            if (double.IsNaN(finalRect.X) || double.IsNaN(finalRect.Y) || double.IsNaN(finalRect.Width) || double.IsNaN(finalRect.Height))
                throw new ArgumentException("NaN not allowed", "finalRect");

            // update the element's state
            ElementState state;

            if (states.ContainsKey(e))
            {
                state = states[e];
            }
            else
            {
                // get the starting location of the item
                Rect initialRect = GetInitialRect(DesiredSize, finalRect, e);

                state = new ElementState(finalRect, initialRect, Randomness);
                states.Add(e, state);
            }

            // set the final rectangle and kick off the timer
            if (isAnimated && IsAnimationEnabled)
            {
                if (!state.NeedsArrange)
                {
                    state.NeedsArrange = true;
                    animatingCount++;
                    globalAnimatingCount++;
                }

                state.TargetRect = finalRect;

                // make sure that the timer is running
                StartTimer();
            }
            else
            {
                state.CurrentRect = finalRect;

                if (state.NeedsArrange)
                {
                    state.NeedsArrange = false;
                    animatingCount--;
                    globalAnimatingCount--;
                }
            }

            e.Arrange(state.CurrentRect); // We must always call this when re-arranging (and call it at the end after deciding what the CurrentRect is)
        }

        #endregion Protected Methods

        #region Virtual Methods

        protected virtual Rect GetInitialRect(Size panelSize, Rect elementRect, UIElement element)
        {
            return new Rect(0, 0, elementRect.Width, elementRect.Height);
        }

        #endregion Virtual Methods

        #region Nested Classes


        private class ElementState
        {

            #region Static Fields

            public static readonly Random _Random = new Random();

            #endregion Static Fields

            #region Fields

            public bool NeedsArrange;
            public readonly double AttractionMultiplier;

            public Rect TargetRect;
            public Rect CurrentRect;
            public Vector LocationVelocity;
            public Vector SizeVelocity;

            #endregion Fields

            #region Constructors

            public ElementState(Rect targetRect, Rect initialRect, double randomness)
            {
                CurrentRect = initialRect; // new Rect(0, 0, targetRect.Width, targetRect.Height);
                TargetRect = targetRect;
                LocationVelocity = new Vector(0, 0);

                SizeVelocity = new Vector(0, 0);

                AttractionMultiplier = (_Random.NextDouble() * randomness) + .4;
                NeedsArrange = false;
            }

            #endregion Constructors
        }

        #endregion Nested Classes

        #region Force (DependencyProperty)

        public double Force
        {
            get { return (double)GetValue(ForceProperty); }
            set { SetValue(ForceProperty, value); }
        }
        public static readonly DependencyProperty ForceProperty =
            DependencyProperty.Register("Force", typeof(double), typeof(AnimatingPanelBase), null);

        #endregion

        #region Resistance (DependencyProperty)

        public double Resistance
        {
            get { return (double)GetValue(ResistanceProperty); }
            set { SetValue(ResistanceProperty, value); }
        }
        public static readonly DependencyProperty ResistanceProperty =
            DependencyProperty.Register("Resistance", typeof(double), typeof(AnimatingPanelBase), null);

        #endregion

        #region Milliseconds (DependencyProperty)

        public double Milliseconds
        {
            get { return (double)GetValue(MillisecondsProperty); }
            set { SetValue(MillisecondsProperty, value); }
        }
        public static readonly DependencyProperty MillisecondsProperty =
            DependencyProperty.Register("Milliseconds", typeof(double), typeof(AnimatingPanelBase), null);

        #endregion

        #region SnapPrecision (DependencyProperty)

        /// <summary>
        /// A description of the property.
        /// </summary>
        public double SnapPrecision
        {
            get { return (double)GetValue(SnapPrecisionProperty); }
            set { SetValue(SnapPrecisionProperty, value); }
        }
        public static readonly DependencyProperty SnapPrecisionProperty =
            DependencyProperty.Register("SnapPrecision", typeof(double), typeof(AnimatingPanelBase), null);

        #endregion

        #region Randomness (DependencyProperty)

        public double Randomness
        {
            get { return (double)GetValue(RandomnessProperty); }
            set { SetValue(RandomnessProperty, value); }
        }
        public static readonly DependencyProperty RandomnessProperty =
            DependencyProperty.Register("Randomness", typeof(double), typeof(AnimatingPanelBase), null);

        #endregion

        #region IsAnimationEnabled (DependencyProperty)

        public bool IsAnimationEnabled
        {
            get { return (bool)GetValue(IsAnimationEnabledProperty); }
            set { SetValue(IsAnimationEnabledProperty, value); }
        }
        public static readonly DependencyProperty IsAnimationEnabledProperty =
            DependencyProperty.Register("IsAnimationEnabled", typeof(bool), typeof(AnimatingPanelBase), null);

        #endregion

        #region AnimationCompleted

        public event RoutedEventHandler AnimationCompleted;

        private void RaiseAnimationCompleted()
        {
            StopTimer();

            if (AnimationCompleted != null)
            {
                AnimationCompleted(this, new RoutedEventArgs());
            }
        }

        #endregion

        #region GlobalAnimationCompleted

        public static event RoutedEventHandler GlobalAnimationCompleted;

        private static void RaiseGlobalAnimationCompleted()
        {
            if (GlobalAnimationCompleted != null)
            {
                GlobalAnimationCompleted(null, new RoutedEventArgs());
            }
        }

        #endregion


		public void Dispose()
		{
			IsAnimationEnabled = false;
			StopTimer();
		}
    }
}
